Глава 19
КОЛЛЕКЦИИ
Одним из недостатков языка Паскаль (и Турбо Паскаль) является невозможность создания и использования в программе массивов с переменной размерностью - так называемых динамических массивов. Этот недостаток особенно ощутимо сказывается в диалоговых программах, работа которых в существенной своей части определяется действиями пользователя. Если программист заранее не знает, какие именно требования к используемым в программе массивам предъявит пользователь, он обычно резервирует для них максимально возможные объемы памяти или размещает массивы в куче. И тот и другой способы нельзя считать вполне удовлетворительными: в первом случае возникают неестественные ограничения на предельно возможные размеры массивов или выделенная под их размещение память расходуется нерационально, во втором случае приходится прибегать к дополнительным ухищрениям, чтобы организовать индексированный доступ к динамической памяти.
Разработчики Turbo Vision решили проблему кардинально, создав механизм коллекций. Хотя экземпляры объектов можно объединять в массивы, как и «обычные» переменные Паскаля, Вы вряд ли захотите использовать массивы для их хранения: коллекции не только снимают проблемы фиксированных границ, но имеют еще и целый ряд новых возможностей, которыми массивы не обладают.
Изучению коллекций посвящается эта глава.
Коллекции предназначены, строго говоря, для тех же целей, что и массивы языка Турбо Паскаль: они позволяют создать набор из произвольного количества элементов и организуют индексный способ доступа к этим элементам. В отличие от массивов коллекции обладают двумя новыми свойствами. Во-первых, их размер может динамически меняться в ходе работы программы, фактически ограничиваясь лишь доступной памятью. Во-вторых, в коллекции могут храниться элементы разных типов. Последнее свойство называется полиморфизмом коллекций.
Технически коллекции представляют собой массивы нетипизированных указателей на размещенные в динамической памяти элементы коллекций. Эти массивы размещаются в куче - отсюда возможность динамического изменения размеров коллекций; с другой стороны, входящие в эти списки указатели позволяют ссылаться на произвольные элементы, отсюда полиморфизм.
Полиморфизм коллекций - это очень мощное средство, которым следует пользоваться с осторожностью, поскольку фактически коллекция хранит лишь указатели на элементы. Компилятор не может проверить правильность доступа к элементам; Вы можете поместить в коллекцию один объект, а взять его из коллекции как объект другого типа и компилятор не сможет предупредить Вас об этом.
Для создания коллекции следует прежде всего определить тип тех данных, которые будут в ней храниться. Например, Вам необходимо создать электронный каталог Вашей личной библиотеки. В этом случае для каждой указанной в каталоге книги можно выделить пять полей данных: автор, название, издательство, год издания и количество страниц. Создадим новый объект TBook следующего вида:
Uses Objects;
type
PBook = ^TBook;
TBook = object (TObject)
Autor: PSbring; {Автор}
Title: PString; {Название}
PubHouse: PString; {Издательство}
Year: Word; {Год издания}
Pages: Word; {Количество страниц}
Constructor Init(A,T,PH: String; Y,P: Word);
Destructor Done; Virtual;
end ;
Мы включили' в объект поля и два основных метода работы с ними: конструктор Init используется для размещения в динамической памяти очередной записи каталога, деструктор Done удаляет записи из кучи. Заметим, что в объекте задаются не сами текстовые строки, а указатели на них (тип PString в Turbo Vision описан как указатель на тип String). Такое размещение данных как правило дает значительную экономию памяти. Например:
Constructor TBook.Init(А,Т,РН: String; Y,P: Word);
begin
Autor := NewStr(A);
Title := NewStr(T);
PubHouse := NewStr(PH);
Year := Y;
Pages := P
end; {TBook.Init}
Используемые в конструкторе функции NewStr размещают в динамической памяти текстовую строку, выделяя под нее минимально необходимую память, что значительно выгоднее по сравнению с типичным описанием текстовых полей вида
type
TBook = object (TObject)
Autor, Title, PubHouse: String;
.....
end;
Для освобождения динамической памяти в деструкторе Done используется процедура DisposeStr:
Destructor TBook.Done;
begin
DisposeStr(Autor);
DisposeStr(Title);
DisposeStr (PubHouse)
end; {TBook.Init}
После того как тем или иным способом определены типы данных, создание коллекции не вызывает проблем. Например:
var
BookList: PCollection;
begin
BookList := New(PCollection, Init(50,10));
with BookList^ do
begin
Insert(New(PBook, Init('Джордейн Р.',
'Справочник программиста персональных компьютеров'+
' типа IBM PC, XT и AT','Финансы и статистика',
1991,544)));
Insert(New(PBook, Init('Шелдон',
'Язык Си для профессионалов','И.В.К.-СОФТ',1991,383))); Insert(New(PBook, Init('Скэнлон Л.',
'Персональные ЭВМ IBM PC и XT. '+
'Программирование на языке ассемблера',
'Радио и связь',1991,336)));
Insert(New(PBook,
Init('Йенсен К., Вирт Н.',
'Паскаль. Руководство для пользователя '+
'и описание языка','Финансы и Статистика',1982,151)));
end ;
.....
Dispose(BookList, Done);
end;
Для создания коллекции мы обратились к методу TCollection.Init, указав ему начальную длину коллекции (50 элементов) и шаг наращивания (10 элементов). Руководствуясь этими указаниями, Turbo Vision зарезервирует в динамической памяти место для размещения 50 указателей. Если в ходе наполнения коллекции ее длина превысит начальную, Turbo Vision будет наращивать коллекцию порциями, каждая из которых достаточна для размещения 10 указателей.
Смысл параметров, передаваемых методу TCollection.Init, станет понятнее, если рассмотреть механизм создания и обновления коллекции. Вначале в куче резервируется участок памяти, достаточный для размещения массива из N0 указателей (N0 - начальный размер коллекции). Если в ходе наполнения коллекции ее длина превысит N0 элементов, резервируется новый участок памяти, достаточный для размещения массива из NO + DN указателей (DN - шаг наращивания коллекции), затем старый массив переносится на новое место, а память, выделенная под его размещение, возвращается в кучу. Таким образом, чем больше начальная длина коллекции и шаг ее наращивания, тем меньше суммарные потери времени на расширение коллекции, но и тем больше могут стать потери памяти, есди реальная длина коллекции окажется значительно меньше NO + k*DN (k = О, 1, 2,...).
Операторы Insert размещают в динамической памяти элементы коллекции. В реальной программе наполнение коллекции будет, судя по всему, осуществляться каким-то иным способом, чем простое программирование обращений к методу Insert (см., например, программу Notebook из гл.15). Для нас сейчас важно другое: мы нигде не говорили коллекции, какого типа объекты она будет хранить; для обеспечения нужных действий по размещению в памяти очередного элемента мы просто обращаемся к соответствующему методу Init, а уж он делает остальное - ведь он «знает» как это следует сделать.
Отметим, что обращение
Dispose(BookList, Done);
вызывает автоматическое обращение к методу TBook.Done перед уничтожением каждого элемента коллекции, после чего уничтожается экземпляр TCollection. Это стало возможным потому, что объект TBooh объявлен нами как потомок от TObject. Если бы мы его объявили независимым объектом
type
TBook = object
.....
end;
мы должны были бы сами позаботиться об освобождении кучи, а обращение
Dispose(BookList, Done);
привело бы к «зависанию» программы.
19.3. ДОСТУП К ЭЛЕМЕНТАМ КОЛЛЕКЦИЙ
Итак, оператором
BookList := New(PCollection, Init(50,10));
мы объявили о создании коллекции, а операторами
Insert(New(PBook, Init(...)))
наполнили эту коллекцию нужными элементами. Как осуществить доступ к элементам коллекции? Для этого можно использовать несколько способов.
Во-первых, к любому элементу коллекции можно обратиться по его порядковому номеру (индексу). В отличие от массивов Турбо Паскаля, индексы которых могут иметь произвольные границы, коллекции индексируются целыми числами в диапазоне от 0 до Count-l (Count - общее количество элементов в коллекции). Любая коллекция имеет поле Count, которое указывает текущую длину коллекции. Чтобы по индексу получить доступ к нужному элементу, используется метод At, который возвращает указатель на элемент.
Например, нам требуется вывести на экран содержимое тех записей нашего каталога, которые соответствуют 1991 году издания. Тогда вместо точек в конце программы, показанной на с.426, следует вставить оператор
PrintYear(BookList,1991);
Кроме того, в раздел описаний программы добавим две новых процедуры:
Procedure PrintItem(A,T,PB: String; Y,P: Word);
{Выводит на экран элемент коллекции}
begin
WriteLn(A); WriteLn(' ',Т):
WriteLnC ',РВ,', ',Y,', ',Р)
end; {PrintItem}
Procedure PrintYear(BookList: PCollection; Y: Word);
{Выводит на экран издания нужного года выпуска}
var
Book: PBook;
k: Integer;
begin
WriteLn;
for k := 0 to pred(BookList^.Count) do
begin
Book := BookList^.At(k);
with Book^ do if Year = Y then
PrintItem(Autor^,Title^,PubHouse^,Year,Pages)
end
end; {PrintYear}
В процедуре PrintYear организуется счетный цикл от 0 до pred (TCollection.Count). С помощью оператора
Book := BookList^.At(k);
в переменную Book помещается указатель на элемент коллекции с индексом k. Именно здесь обеспечивается полиморфизм коллекций: метод At возвращает нетипизированный указатель, который Вы можете интерпретировать нужным Вам образом. Однако здесь же таится источник трудно обнаруживаемых ошибок: в левую часть оператора присваивания можно поместить указатель любого типа и тип этого указателя может не соответствовать типу k-гo элемента коллекции.
Отметим, что обращение к методу At с индексом, выходящим за границы коллекции, активизирует вызов метода TCollection.Error, который по умолчанию аварийно завершает исполнение программы (подробнее см. п. 17.6).
Помимо использования метода At коллекции обеспечивают доступ к трем итерационным методам, которые могут оказаться весьма удобными. Метод ForEach осуществляет некоторую заранее заданную операцию сразу над всеми элементами коллекции, а методы FirstThat и LastThat отыскивают в коллекции первый элемент, удовлетворяющий некоторому опять же заранее заданному критерию поиска: FirstThat ищет от начала коллекции к ее концу, a LastThat - в обратном направлении.
Чтобы воспользоваться методом ForEach мы должны сначала создать процедуру без параметров, которая осуществляет нужные действия над всеми элементами коллекции, и передать адрес этой процедуры как параметр обращения к ForEach. Например, для того чтобы вьшести на экран содержимое всего нашего каталога, мы должны внести в программу следующие изменения. Поместите в раздел описаний следующий текст процедуры PrintAll:
Procedure PrintAll (Book: PCollection) ;
{Вывод всех элементов коллекции}
Procedure PrintBook(P: PBook) ; far;
begin
with Р^ do
PrintItem (Autor^,Title^, PubHouse^, Year, Pages)
end; {PrintBook}
begin {PtintAll}
WriteLn;
Book^ .ForEach (@PrintBook) ;
end; {PrintAll}
Как видите, эта процедура содержит внутреннюю процедуру PrintBook, в которой осуществляется нужное нам действие - вывод очередного элемента каталога на экран. Этот вывод достигается с помощью вызова уже использованной нами ранее процедуры PrintItem. Таким образом, описание процедуры PrintAll должно следовать после описания PrintItem, чтобы этот вызов был синтаксически правильным. Далее, вывод всех элементов коллекции в процедуре PrintAll осуществляется оператором
Book^. ForEach (@PrintBook) ;
который обращается к методу TCollection.ForEach, передавая ему в качестве параметра адрес процедуры PrintBook. Чтобы программа успешно выполнила нужные действия, процедура, адрес которой передается методу ForEach, должна удовлетворять двум условиям:
PrintAll (BookList) ;
перед оператором уничтожения коллекции. Если Вы запустите таким образом подготовленную программу на счет, на экран будет выведено:
Джордейн Р .
Справочник программиста персональных компьютеров типа IBM PC,
XT и AT
Финансы и статистика, 1991, 544
Шелдон
Язык Си для профессионалов
И. В. К. -СОФТ, 1991, 383
Скэнлон Л.
Персональные ЭВМ IBM PC и XT. Программирование на языке ассемблера
Радио и связь, 1991, 336
Йенсен К., Вирт Н.
Паскаль . Руководство для пользователя и описание языка
Финансы и статистика, 1982, 151
Все действия по выводу содержимого коллекции реализуются методом ForEach, который для собственно вывода каждого очередного элемента обращается к процедуре PrintBook.
Точно таким же образом реализуется обращение к методам FirstThat и LastThat. Например, если нам потребуется отыскать в каталоге запись, содержащую слово «Вирт» в noлe Autor, можно добавить в программу следующие строки
Procedure SearchAutor(BookList: Pcollection; A: String);
Function FindAutor(P: PBook): Boolean; far;
begin
FindAutor := pos(А, Р^.Autor^) <>0
end; {FindAutor}
var
Book: PBook;
begin {SearchAutor}
Book := BookList^.FirstThat(@FindAutor);
if Book = NIL then
WriteLn('Heт автора ',А)
else with Book^ do
begin
WriteLn;
PrintItern(Autor^,Title^,PubHouse^,Year,Pages)
end
end; {SearchAutor}
В тело главной программы следует добавить оператор
SearchAutor(BookList,'Вирт');
Собственно поиск элемента коллекции реализуется оператором
Book := BookList^.FirstThat(@FindAutor);
который для этих целей обращается к методу TCollection.FirstThat. В этом методе реализуется последовательный анализ всех элементов коллекции, начиная с самого первого (с индексом 0), причем для анализа используется вызов функции FindAutor. Как видим, эта функция нужным образом анализирует очередную запись и возвращает True, если условие поиска удовлетворено. Метод FirstThat возвращает указатель на элемент коллекции, для которого удовлетворено условие поиска, или NIL, если этому условию не отвечает ни один элемент. Таким образом, оператор
if Book = NIL then
.....
else
.....
проверяет результат поиска и выводит на печать найденный элемент коллекции или сообщение «Нет автора ...», если условие поиска не удовлетворено.
Как и в случае метода ForEach, функция, передаваемая методу FirstThat, должна транслироваться в расчете на дальнюю модель памяти и должна локализоваться в теле процедуры, в которой осуществляется вызов метода.
Любой элемент коллекции можно удалить или заменить новым. Для удаления используется метод AtFree, которому в качестве параметра передается индекс удаляемого элемента. При удалении элемента предполагается, что коллекция содержит указатели на объекты, порожденные от TObect и размещенные в куче, поэтому автоматически вызывается метод TObject.Done. Индексы всех элементов, размещенных в коллекции после удаляемого элемента, уменьшаются на 1.
С помощью метода DeleteAll удаляются все элементы из коллекции, но сама коллекция при этом сохраняется, т.е. очищенная коллекция будет иметь Count = 0. Для очистки коллекции вызывается AtFree для каждого элемента.
Чтобы заменить существующий элемент новым, используется метод AtPut (Index, Item), где Index - индекс заменяемого элемента, a Item - указатель на новый элемент.
Метод Atlnsert (Index, Item) вставляет новый элемент в коллекцию в позицию Index и увеличивает индексы всех ранее существовавших в коллекции элементов от элемента Index до конца коллекции на единицу, т.е. «раздвигает» коллекцию.
19.4. ОТСОРТИРОВАННЫЕ КОЛЛЕКЦИИ
Часто бывает необходимо каким-либо образом упорядочить коллекцию, т.е. расставить ее элементы в определенном порядке. Для этих целей в Turbo Vision предусмотрен специальный объект TSortedCollection. Этот объект порожден от TCollection и, следовательно, уже умеет создавать коллекцию, вставлять в нее элементы и удалять их. Единственное, чего он не умеет - это сортировать коллекцию. В TSortedCollection есть абстрактный метод Compare, который используется для упорядочения элементов и который Вы должны перекрыть, чтобы обеспечить нужную Вам сортировку. Таким образом, чтобы создать отсортированную коллекцию, Вы должны создать объект-потомок от TSortedCollection и перекрыть его метод Compare.
По умолчанию этот метод получает в качестве параметров указатели на два элемента коллекции и должен вернуть 1, 0 или -1 в зависимости от того, больше, равно или меньше какое-то поле первого элемента по сравнению с этим же полем второго элемента. Поле, по которому сравниваются элементы, называется ключевым.
Например, нам требуется создать отсортированную коллекцию, содержащую каталог библиотеки (см. пример п. 19.3), причем в качестве ключевого используется поле Autor^. Тогда создадим новый объект
type
PSort = ^Tsort;
TSort = object (TSortedCollection)
Function Compare(Key1, Key2: Pointer): Integer; Virtual;
end;
чтобы перекрыть метод Compare. Если теперь объявить новый метод TSort.Compare следующим образом:
Function TSort.Compare(Key1, Key2: Pointer): Integer;
var
A: PSort absolute Key1;
B: PSort absolute Key2;
begin
if A^.Autor^ < BA.Autor^ then
Compare := -1 else if A^.Autor^ = B^.Autor^ then
Compare := 0
else
Compare := 1
end; {TSort.Compare}
то после объявления
var
BookList: PSort;
вместо
var
BookList: PCollection;
программа выведет каталог, отсортированный по фамилиям авторов:
Джордейн Р.
Справочник программиста персональных компьютеров типа IBM PC, XT и AT
Финансы и статистика, 1991, 544 Йенсен К., Вирт Н.
Паскаль. Руководство для пользователя и описание языка финансы и статистика, 1982, 151 Скэнлон Л.
Персональные ЭВМ IBM PC и XT. Программирование на языке ассемблера
Радио и связь, 1991, 336 Шелдон
Язык Си для профессионалов И.В.К.-СОФТ, 1991, 383
Ключевое поле определяется методом TSortedCollection.KeyOf. Этот метод по заданному в качестве параметра обращения указателю на элемент коллекции возвращает указатель на ключевое поле. По умолчанию метод KeyOf возвращает указатель на весь элемент, однако Вы можете перекрыть его новым методом, возвращающим указатель на нужное ключевое поле. Пусть, например, нам требуется отсортировать каталог по году издания книг (поле Year). Добавим в описание объекта TSort перекрытие метода KeyOf:
type
TSort = object (TSortedCollection)
.....
Function KeyOf(Item: Pointer): Pointer; Virtual;
end ;
Опишем новый метод следующим образом:
Functon TSort.KeyOf(Item: Pointer): Pointer;
begin
KeyOf := @PBook(Item)^.Yеаr
end;
и изменим описание метода Compare:
Function TSort.Compare(Key1, Key2: Pointer): Integer;
var
A: ^Integer absolute Key1;
B: ^Integer absolute Key2;
begin
if А^ < B^ then
Compare := -1
else if А^ = B^ then
Compare := 0
else
Compare := 1
end; {TSort.Compare}
Теперь после запуска программы на экран будет выведено:
Йенсен К., Вирт Н.
Паскаль. Руководство для пользователя и описание языка
Финансы и статистика, 1982, 151
Джордейн Р.
Справочник программиста персональных компьютеров типа IBM PC,
XT и AT
Финансы и статистика, 1991, 544
Обратите внимание: в отсортированной коллекции теперь хранятся только 2 элемента! Произошло это потому, что по умолчанию TSortedCollection игнорирует новую запись, если в коллекции уже существует элемент, ключевое поле которого имеет такое же значение. Таким образом, обычно в отсортированной коллекции содержатся записи с уникальными ключевыми полями.
Можно ли поместить в коллекцию два или больше элементов с одинаковыми полями? Turbo Vision позволяет сделать это: поле TSortedCollection.Duplicates по умолчанию содержит FALSE, что указывает на уникальность ключевого поля; если перед наполнением коллекции Вы поместите в это поле значение TRUE, коллекция не будет контролировать уникальность ключевых полей.
Изменим начало раздела исполняемых операторов главной программы следующим образом:
begin
Bookiist := New(PSort, Init(50,10) ) ;
with BookList^ do
begin
Duplicates := True; {Отменяем уникальность ключей}
......
end;
.....
end.
Теперь на экран будет выведено:
Йенсен К., Вирт Н.
Паскаль. Руководство для пользователя и описание языка Финансы и статистика, 1982, 151 Скэнлон Л.
Персональные ЭВМ IBM PC и XT. Программирование на языке ассемблера
Радио и связь, 1991, 336 Шелдон
Язык Си для профессионалов И.В.К.-СОФТ, 1991, 383 Джордейн Р.
Справочник программиста персональных компьютеров типа IBM PC, XT и AT Финансы и статистика, 1991, 544
Заметим, что, очередной элемент вставляется перед первым элементом с равным значением ключевого поля. Точно также поиск First.That вернет указатель на первый из нескольких элементов с одинаковыми ключевыми полями, а метод LastThat - на последний из них.
Для создания и использования коллекции отсортированных строк в Turbo Vision используется объект TSrtingCollection. Этот объект является прямым потомком от TSortedCollection и отличается от него тем, что его метод Compare не является абстрактным - по умолчанию он осуществляет обычное для Турбо Паскаля лексикографическое сравнение двух строк. Таким образом, если Вам необходимо отсортировать коллекцию строк по алфавиту (точнее, в соответствии с внутренней кодировкой символов), Вы можете использовать экземпляр объекта TSortedCollection без какого-либо перекрытия его методов.
В следующей программе создается словарь слов, входящих в некоторый текстовый файл. По умолчанию используется файл с текстом программы, но Вы можете указать имя любого текстового файла в качестве параметра вызова программы.
Uses Objects;
var
f: file of Char; Function OpenFile(var Name: String): Boolean;
{Возвращает FALSE, если нельзя открыть файл}
begin
if ParamCount = 1 then
Name :=" ParamStr(1) {Первый параметр в строке вызова программы должен содержать имя файла,}
else , {если это не так, анализируется файл, содержащий текст программы}
Name := copy(ParamStr(0),1,
posC . ' ,ParamStr(0))) + 'PAS' ;
Assign(f, Name);
{$I-}
Reset(f);
{$I+}
OpenFile := IOResult=0
end; {OpenFile}
Function GetWord: String;
{Получает из файла очередное слово}
var
с: Char;
w: String;
Function Letter(var c: Char): Boolean;
{Возвращает TRUE, если символ - буква}
begin
с := UpCase (с);
{проверяем на строчную русскую букву:}
if с in ['а'..'п'] then . {а - русская буква}
с := chr(ord(c)-ord('а')+ord('А') ) {А - русская буква}
else if с in ['р'..'я'] then , {р - русская буква}
с := chr(ord(с)-ord('р')+ord('Р')) ; {Р - русская буква}
{Проверяем на заглавную букву:}
Letter := с in ['А'..'Z','А1..'Я']
end; {Letter}
begin {GetWord}
w : = ' ' ;
С := #0;
while not EOF(f) and not Letter(c) do
Read(f,c);
if not EOF(f) then while not EOF(f) and Letter(c) do
begin
w .:= w+c;
Read(f,c)
end ;
GetWord := w
end; {GetWord}
Procedure PrintList(List: PStringCollection);
{Выводит на экран список слов}
Procedure PrintWord(p: PString); far;
begin
Write(р^, ' ':20-Length(р^))
end; {PrintWord}
begin {PrintList}
WriteLn;
WriteLn;
List^.ForEach(@PrintWord);
WriteLn
end; {PrintList}
var
WordList: PStringCollection;
w: String;
begin {Основная программа}
if not OpenFile(w) then
WriteLn('Нельзя открыть файл '+w)
else
begin
WordList := New(PStringCollection, Init(200,10));
repeat
w := GetWord;
if (w <> ' ') and (MaxAvail > 255) then
WordList^.Insert(NewStr(w))
until w='';
PrintList(WordList)
end
end.
Отметим, что в операторе
if (w <> '') and (MaxAvail > 255) then
осуществляется контроль за доступной динамической памятью. В Turbo Vision есть и встроенные способы контроля кучи - см. п. 19.6.
Как и в любой другой отсортированной коллекции, в коллекции строк по умолчанию хранятся элементы с уникальными ключевыми полями. Чтобы подавить контроль за уникальностью строк, добавьте оператор
WordList^.Duplicates := True;
сразу за оператором создания коллекции
WordList := New(PStringCollection, Init(200,10));
и сделайте еще один прогон программы, - Вы увидите, как много раз встречается в файле одно и то же слово.
Метод TStringCollection.Compare следует перекрыть, если Вы хотите осуществить свой способ сортировки строк. Например, используя объект
type
PStrSor =^TStrSor;
TStrSor = object (TStringCollection)
Function Compare(k1, k2: Pointer): Integer; Virtual;
end;
Function TStrSor.Compare(k1, k2: Pointer): Integer;
var
s1: PString absolute k1;
s2: PString.absolute k2;
begin
if s1^< s2^ then
Compare := 1
else if s1^ = s2^ then
Compare := 0
else
Compare := -1
end;
вместо PStringCollection, Вы сможете вывести на экран список слов, отсортированных в обратном порядке.
Как уже говорилось, коллекции Turbo Vision обладают свойством полиморфизма -они позволяют хранить различные объекты. Поскольку каждый объект имеет все необходимые для него поля и методы, работа с полиморфными коллекциями не создает дополнительных проблем. Действительно, в полиморфной коллекции Вам обычно нет нужды следить за тем, какого типа объект хранится в том или ином элементе - достаточно вызвать нужный виртуальный метод, чтобы осуществить над элементом требуемые действия.
Рассмотрим следующий пример. Пусть необходимо создать и использовать библиотеку графических примитивов (точки, окружности, прямоугольники и т.п.). Каждый из этих элементов может отличаться своим набором полей и методов. Однако некоторые методы выполняют над объектами однотипные действия, такие, например, как создание нового объекта или его вычерчивание на экране. Если эти методы сделать виртуальными и инкапсулировать в объект-предок, каждый из его потомков сможет осуществить нужные действия одинаковым способом.
Для нашего примера можно создать следующий объект-родитель:
type
PGraphObject = ^TGraphObject;
TGraphObject = object (TObject)
X, Y: Integer; {Координаты характерной точки}
Constructor Init; {Создание объекта}
Procedure Draw; Virtual; {Вычерчивание}
end;
Объект TGraphObject содержит общие для всех потомков поля и методы. Заметим, что методы Init и Draw должны перекрываться в объектах-потомках, поэтому их содержимое не имеет значения. Однако полезно вынести в них некоторые общие для всех потомков части программы. Например, конструктор Init может помещать в поля X и Y заданные начальные значения; если этот метод наполнить конкретным содержанием, он может использоваться во всех объектах иерархии:
Constructor TGraphObject.Init;
{Присваивает случайные значения координатам X и Y}
begin
X := Random(GetMaxX);
Y := Random(GetMaxY)
end;
Здесь GetMaxX, GetMaxY - максимальные координаты графического экрана. Виртуальный метод Draw весьма специфичен: его конкретная программная реализация будет существенно зависеть от типа объекта. Поэтому объявим этот метод абстрактным:
Procedure TGraphObject.Draw;
{Абстрактный метод для вычерчивания графического примитива}
begin
Abstract
end;
Как видим, тело этого метода содержит обращение к глобальной процедуре Abstract, которая аварийно завершает выполнение программы и выдает соответствующую диагностику, если в программе используется вызов метода TGraphObject.Draw. Таким стандартным способом Turbo Vision сообщает пользователю о некорректности программы. Вы можете сделать тело этого метода другим, если Вас не устраивают стандартные действия, однако во всех случаях имеет смысл предусмотреть возможность некорректного вызова абстрактного метода, даже если вновь создаваемая библиотека будет использоваться только Вами - это значительно облегчит отладку программы.
Создадим три потомка от TGraphObject:
type
PPoint =^TPoint;
TPoint = object (TGraphObject) {Точка}
Procedure Draw; Virtual;
end;
PCircle = ^TCircle; {Окружность}
TCircle = object (TGraphObject)
R: Integer;
Constructor Init;
Procedure Draw; Virtual;
end;
PRectangle = ^TRectangle; {Прямоугольник}
TRectangle = object (TGraphObject)
W, H: Integer;
Constructor Init;
Procedure Draw; Virtual;
end;
Объект TPoint (точка) не имеет новых полей и поэтому лишь перекрывает абстрактный метод TGraphObject.Draw:
Procedure TPoint.Draw; {Выводит точку на экран}
begin
PutPixel (X, Y, White)
end;
В объектах TCircle (окружность) и TRectangle (прямоугольник) инкапсулированы новые поля, поэтому перекрываются также и методы Init:
Constructor TCircle.Init;
{Создает окружность случайного радиуса в случайном месте}
begin
TGraphObject.Init; {Получаем координаты центра}
R := Random(GetMaxY div 2) {Получаем радиус}
end;
Procedure TCircle.Draw;
{Вычерчивает окружность}
begin
Circle(X, Y, R)
end;
Constructor TRectangle.init;
{Создает случайный прямоугольник}
begin
TGraphObject.Init;{Верхний левый угол}
W := Random(GetMaxX div 2) {Ширина}
H := Random(GetMaxY div 2) {Высота}
end;
Procedure TRectangle.Draw;
{Вычерчивает прямоугольник}
begin
Rectangle(X, Y, X+W, Y+H)
end;
После того как определены нужные объекты, не составляет особого труда поместить эти объекты в коллекцию и вывести их на экран. Например, для вывода всех элементов коллекции можно использовать такую процедуру:
Procedure DrawAll (C: PCollection) ;
{Выводит все элементы полиморфной коллекции}
Procedure DrawItem(p: PGraphObject); far;
begin
p^.Draw {Это и есть полиморфизм в действии!}
end;
begin
С^.ForEach(@DrawItem)
end;
Как видим, в процедуре DrawItem полиморфизм используется дважды: во-первых, метод ForEach обращается к ней, передавая в качестве параметра обращения нетипизированный указатель на элемент коллекции; это позволяет трактовать параметр как указатель на любой объект, в том числе и на TGraphObject. Во-вторых, в процедуре используется обращение к виртуальному методу объекта-родителя Draw: поскольку этот метод перекрывается во всех потомках, каждый из них будет использовать свой метод Draw для вывода на экран.
Сформируем программу, поместив в нее вместо, точек уже рассмотренные фрагменты:
Uses Objects,Graph,CRT;
type
.....
Constructor TGraphObject.Init;
.....
Procedure TGraphObject.Draw;
.....
Constructor TPoint.Init;
.....
Procedure TPoint.Draw;
.....
Constructor TCircle.Init;
.....
Procedure TCircle.Draw;
.....
Constructor TRectangle.Init;
.....
Procedure TRectangle.Draw;
.....
Procedure DrawAll(C: PCollection);
.....
var
a, r, k: Integer;
List: PCollection;
p: Pointer;
begin . a := 0;
{Инициируем графический режим работы экрана:}
InitGraph(a, r, '\TP\BGI');
r := GraphResult; if r <> 0 then
WriteLn(GraphErrorMsg(r)) {Ошибка инициации}
else
begin
{Создаем коллекцию:}
List := New(PCollection, Init (20,5));
{Наполняем ее 20 элементами:}
for k := 1 to 20 do
begin
case k mod 3 of
0: p := New(PPoint, Init);
1: p := New(PCircle, Init);
2: p := New(PRectangle, Init)
end;
if p <> NIL then List^.Insert(p)
end ;
DrawAll(List) ; {Выводим на экран все элементы}
While not KeyPressed do;{Ждем нажатия на любую клавишу}
CloseGraph {Возвращаемся в текстовый режим}
end
end.
В этой программе предполагается, что драйвер графического экрана расположен в каталоге \TF\BGI на текущем диске. Если это не так, следует указать маршрут поиска
этого драйвера в качестве параметра обращения к процедуре InitGraph. Кроме того, каталог, содержащий стандартную графическую библиотеку Graph, должен быть указан опцией Options/Directories/Unit directories, если, разумеется, библиотека не содержится в текущем каталоге.
19.7. КОЛЛЕКЦИИ И УПРАВЛЕНИЕ ПАМЯТЬЮ
Поскольку элементы коллекций располагаются в динамической памяти, при их использовании особенно важными становятся вопросы контроля за состоянием кучи.
Любая коллекция не может расти до бесконечности: с одной стороны, ее размеры определяются доступной памятью и размером элементов, с другой стороны - общее количество элементов коллекции не может превышать
65520 div SizeOf (Pointer) = 16380
Иными словами, все указатели на элементы коллекции должны располагаться в пределах одного сегмента. Величина 16380 задается значением глобальной константы MaxCollectionSize, объявленной в интерфейсной части модуля Objects.
Таким образом, при наполнении коллекции необходимо следить за общим количеством элементов (переменная TCollection.Couni), которое не может превысить значение, задаваемое константой MaxCollectionSize. Кстати, если при обращении к методу TCollection.Init начальное значение N0 коллекции указано слишком большим, оно заменяется на MaxCollectionSize.
Элементы коллекции обычно размещаются в куче, поэтому перед размещением очередного элемента следует проверить доступную память (возвращается стандартной функцией MaxAvail).
В ходе расширения коллекции может оказаться, что динамической памяти не хватает для размещения нового массива указателей (напомню, что расширение коллекции заключается в создании нового массива из NO + k*ND указателей, где N0 - начальная длина, ND - шаг наращивания коллекции, k= 1,2, ...; после этого в новый массив копируется старый массив указателей, а место, выделенное под размещение старого массива, возвращается в кучу). Если обнаружена нехватка памяти для расширения коллекции или если при обращении к методу TCollection.At указан индекс, превышающий размер коллекции, вызывается метод TCollection.Error. По умолчанию этот метод завершает выполнение программы с кодом 212 ошибки периода исполнения. Вы можете перекрыть TCollection.Error, чтобы нужным образом отреагировать на возникшую ситуацию. В этом случае следует учесть, что заголовок TCollection.Error в Turbo Vision имеет следующий вид:
Procedure TCollection.Error (Code, Info': Integer);
Параметр Code в стандартных ситуациях соответствует следующим константам: